package com.conversationboard.fulltext.search;

import java.io.IOException;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Hits;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.joda.time.DateTime;

import com.conversationboard.cache.ThreadTitleCache;
import com.conversationboard.config.Configuration;
import com.conversationboard.fulltext.search.hit.MessageHit;
import com.conversationboard.fulltext.search.hit.ThreadHit;
import com.conversationboard.instrumentation.ObjectSize;
import com.conversationboard.model.Board;
import com.conversationboard.model.Boards;
import com.conversationboard.model.User;

public class Search {

	public static final int MAX_SEARCH_RESULTS_PER_PAGE = 10;

	private static IndexSearcher threadSearcher;
	private static IndexSearcher messageSearcher;

	private static boolean threadIndexCorrupt = false;
	private static boolean messageIndexCorrupt = false;

	private Date startDate;
	private Date endDate;

	private int boardId;

	static {
		reinitialiseSearchers();
	}


	public static String escapeQueryString(String queryString) {

		queryString = queryString.replaceAll("\\!", "\\\\!");
		queryString = queryString.replaceAll("\\(", "\\\\(");
		queryString = queryString.replaceAll("\\)", "\\\\)");
		queryString = queryString.replaceAll("\\[", "\\\\[");
		queryString = queryString.replaceAll("\\]", "\\\\]");
		queryString = queryString.replaceAll("\\~", "\\\\~");
		queryString = queryString.replaceAll("\\?", "\\\\?");
		queryString = queryString.replaceAll("\\^", "\\\\^");
		queryString = queryString.replaceAll("\\+", "\\\\+");
		queryString = queryString.replaceAll("\\-", "\\\\-");

		return queryString;
	}


	public int getBoardId() {
		return boardId;
	}


	public void setBoardId(int boardId) {
		this.boardId = boardId;
	}


	public Date getStartDate() {
		return startDate;
	}


	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}


	public Date getEndDate() {
		return endDate;
	}


	public void setEndDate(Date endDate) {
		this.endDate = endDate;
	}


	public static long getThreadSearcherSizeInKilobytes() {
		return ObjectSize.getSizeInKilobytes(threadSearcher);
	}


	public static long getMessageSearcherSizeInKilobytes() {
		return ObjectSize.getSizeInKilobytes(messageSearcher);
	}


	public static void reinitialiseSearchers() {

		try {
			threadSearcher = new IndexSearcher(Configuration.getInstance().getThreadIndexPath());
		} catch (CorruptIndexException e) {
			threadIndexCorrupt = true;
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		try {
			messageSearcher = new IndexSearcher(Configuration.getInstance().getMessageIndexPath());
		} catch (CorruptIndexException e) {
			messageIndexCorrupt = true;
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}


	public static boolean isThreadIndexCorrupt() {
		return threadIndexCorrupt;
	}


	public static boolean isMessageIndexCorrupt() {
		return messageIndexCorrupt;
	}


	public List<MessageHit> searchMessages(User user, String queryString, int startAt, int numberOfResults, SortOrder sortOrder) throws ParseException, java.text.ParseException, CorruptIndexException, IOException, SQLException {

		List<MessageHit> messageHits = new LinkedList<MessageHit>();

		if (messageSearcher == null) {
			return messageHits;
		}

		queryString = processDateRange(queryString);
		queryString = processBoardId(queryString);

		QueryParser parser = new QueryParser("text", new StandardAnalyzer());
		Query query = parser.parse(queryString);

		/* Sort the results in the query */

		Hits hits = null;

		if (sortOrder == SortOrder.Ascending) {

			SortField[] dateTimeSortField = new SortField[2];
			dateTimeSortField[0] = new SortField("date", SortField.STRING, false);
			dateTimeSortField[1] = new SortField("time", SortField.STRING, false);
			Sort dateTimeSorter = new Sort(dateTimeSortField);

			hits = messageSearcher.search(query, dateTimeSorter);

		} else if (sortOrder == SortOrder.Descending) {

			SortField[] dateTimeSortField = new SortField[2];
			dateTimeSortField[0] = new SortField("date", SortField.STRING, true);
			dateTimeSortField[1] = new SortField("time", SortField.STRING, true);
			Sort dateTimeSorter = new Sort(dateTimeSortField);

			hits = messageSearcher.search(query, dateTimeSorter);

		} else if (sortOrder == SortOrder.Relevance) {
			hits = messageSearcher.search(query);
		}

		if (startAt > hits.length()) {
			return messageHits;
		}

		/* Look at only the page of results we're interested in, starting at start at, and for the next 50 or whatever we're set to */

		for (int i = startAt; i < (startAt + Search.MAX_SEARCH_RESULTS_PER_PAGE); i++) {

			if (i >= hits.length()) {
				break;
			}

			Document document = hits.doc(i);

			MessageHit messageHit = new MessageHit();

			String id = document.get("id");
			String[] components = id.split("-");

			int boardId = Integer.parseInt(components[0]);
			int threadId = Integer.parseInt(components[1]);

			String threadTitle = ThreadTitleCache.getTitle(boardId, threadId);

			messageHit.setBoardId(boardId);
			messageHit.setThreadId(threadId);
			messageHit.setMessageId(Integer.parseInt(components[2]));
			messageHit.setThreadTitle(threadTitle);
			messageHit.setText(document.get("text"));
			messageHit.setDisplayName(document.get("displayName"));
			messageHit.setLoginId(document.get("loginId"));

			String deletedString = document.get("deleted");

			if (deletedString.equals("true")) {
				messageHit.setDeleted(true);
			} else {
				messageHit.setDeleted(false);
			}

			/* This code has a year 2100 problem. It assumes the century is the 2000s. This is to
			 * reduce the size of the date index, which must be loaded into memory by Lucene. */

			String date = "20" + document.get("date");
			SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
			Date d = formatter.parse(date);

			messageHit.setTimestamp(new DateTime(d));

			Board board = Boards.getBoard(messageHit.getBoardId());

			/* Don't add this result if the board is private and the user doesn't have access */

			if (board.isPrivateBoard() && !(user.isMemberOfBoard(board.getBoardId()))) {
				continue;
			}

			/* Don't add results if the board is hidden */

			if (board.isHidden()) {
				continue;
			}

			messageHits.add(messageHit);

		}

		return messageHits;
	}


	private String getLuceneDateFormat(Date date) {

		SimpleDateFormat formatter = new SimpleDateFormat("yyMMdd");
		return formatter.format(date);

	}


	public List<ThreadHit> searchThreads(User user, String queryString, int startAt, int numberOfResults, SortOrder sortOrder) throws ParseException, java.text.ParseException, CorruptIndexException, IOException, SQLException {

		List<ThreadHit> threadHits = new LinkedList<ThreadHit>();

		if (threadSearcher == null) {
			return threadHits;
		}

		queryString = processDateRange(queryString);
		queryString = processBoardId(queryString);

		QueryParser parser = new QueryParser("text", new StandardAnalyzer());
		Query query = parser.parse(queryString);

		Hits hits = null;

		if (sortOrder == SortOrder.Ascending) {
			Sort timeSorter = new Sort(new SortField("date", SortField.STRING, false));
			hits = threadSearcher.search(query, timeSorter);
		} else if (sortOrder == SortOrder.Descending) {
			Sort timeSorter = new Sort(new SortField("date", SortField.STRING, true));
			hits = threadSearcher.search(query, timeSorter);
		} else if (sortOrder == SortOrder.Relevance) {
			hits = threadSearcher.search(query);
		}

		if (startAt > hits.length()) {
			return threadHits;
		}

		/* Process all hits into a list of ThreadHit objects */

		for (int i = startAt; i < (startAt + numberOfResults); i++) {

			if (i >= hits.length()) {
				break;
			}

			Document document = hits.doc(i);

			ThreadHit threadHit = new ThreadHit();

			String id = document.get("id");
			String[] components = id.split("-");

			threadHit.setBoardId(Integer.parseInt(components[0]));
			threadHit.setThreadId(Integer.parseInt(components[1]));
			threadHit.setTitle(document.get("text"));
			threadHit.setDisplayName(document.get("displayName"));
			threadHit.setLoginId(document.get("loginId"));
			String deletedString = document.get("deleted");

			if (deletedString.equals("true")) {
				threadHit.setDeleted(true);
			} else {
				threadHit.setDeleted(false);
			}

			/* This code has a year 2100 problem. It assumes the century is the 2000s. This is to
			 * reduce the size of the date index, which must be loaded into memory by Lucene. */

			String date = "20" + document.get("date");
			SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
			Date d = formatter.parse(date);

			threadHit.setTimestamp(new DateTime(d));

			Board board = Boards.getBoard(threadHit.getBoardId());

			/* Don't add this result if the board is private and the user doesn't have access */

			if (board.isPrivateBoard() && !(user.isMemberOfBoard(board.getBoardId()))) {
				continue;
			}

			/* Don't add this result if the board is hidden */

			if (board.isHidden()) {
				continue;
			}

			threadHits.add(threadHit);

		}

		/* Sort the threads into the right order, if you've specified this. */

		if (sortOrder == SortOrder.Ascending) {
			Collections.sort(threadHits);
		}

		if (sortOrder == SortOrder.Descending) {
			Collections.sort(threadHits);
			Collections.reverse(threadHits);
		}

		return threadHits;
	}


	/* There may be more efficient ways to do this with Lucene. May want to read up and replace later. */

	private String processDateRange(String queryString) {

		if ((this.startDate != null) && (this.endDate != null)) {
			queryString = "date:[" + this.getLuceneDateFormat(startDate) + " TO " + this.getLuceneDateFormat(endDate) + "] AND " + queryString;
		}

		return queryString;
	}


	private String processBoardId(String queryString) {

		if (this.getBoardId() != 0) {
			queryString = "boardId:" + this.getBoardId() + " AND " + queryString;
		}

		return queryString;
	}

}
